今天要讓這些 DTO(Data Transfer Object)真正發揮作用:
搭配 class-validator 與 class-transformer,在 Express 裡自動驗證請求資料。
這一天的重點是:讓「型別」不只是寫給自己看,而是能真的守護 API。
DTO(Data Transfer Object)意思是「資料傳輸物件」,
它的任務很單純:在進入後端邏輯之前,幫你驗證和轉換資料。
例子:
POST /todos
{
"task": "寫鐵人賽 Day 23",
"done": false
}
以前我們會手動檢查:
if (!req.body.task) return res.status(400).json({ error: "task 必填" });
但有了 TypeScript + class-validator,這些檢查能自動完成。
npm install class-validator class-transformer reflect-metadata
在 tsconfig.json
中要開啟幾個設定:
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
在 src/dto/create-todo.dto.ts
:
import { IsString, IsOptional, Length } from "class-validator";
export class CreateTodoDto {
@IsString()
@Length(1, 50, { message: "task 長度必須在 1~50 之間" })
task: string;
@IsOptional()
@IsString()
note?: string;
}
這裡用了幾個 decorator:
@IsString()
:確認是字串@Length(min, max)
:限制字串長度@IsOptional()
:允許欄位不出現建立 src/middleware/validate-dto.ts
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";
import { Request, Response, NextFunction } from "express";
export function validateDto(dtoClass: any) {
return async (req: Request, res: Response, next: NextFunction) => {
const instance = plainToInstance(dtoClass, req.body);
const errors = await validate(instance);
if (errors.length > 0) {
const messages = errors.map(err => Object.values(err.constraints || {})).flat();
return res.status(400).json({ errors: messages });
}
next();
};
}
這個 Middleware 會:
src/index.ts
import "reflect-metadata";
import express from "express";
import { validateDto } from "./middleware/validate-dto";
import { CreateTodoDto } from "./dto/create-todo.dto";
const app = express();
app.use(express.json());
app.post("/todos", validateDto(CreateTodoDto), (req, res) => {
const { task, note } = req.body;
res.json({ message: "Todo 已建立", data: { task, note } });
});
app.listen(3000, () => {
console.log("🚀 Server running on http://localhost:3000");
});
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"task": "學 TypeScript 驗證", "note": "Day 23"}'
輸出:
{
"message": "Todo 已建立",
"data": { "task": "學 TypeScript 驗證", "note": "Day 23" }
}
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"task": ""}'
輸出:
{
"errors": ["task 長度必須在 1~50 之間"]
}
是不是很像 NestJS 那種「自動驗證」?
其實這就是 NestJS 背後的機制,只是這裡是手動加進 Express 的版本。
假設要做 PATCH /todos/:id
,可以這樣定義:
import { IsOptional, IsString, IsBoolean } from "class-validator";
export class UpdateTodoDto {
@IsOptional()
@IsString()
task?: string;
@IsOptional()
@IsBoolean()
done?: boolean;
}
然後在路由:
import { UpdateTodoDto } from "./dto/update-todo.dto";
app.patch("/todos/:id", validateDto(UpdateTodoDto), (req, res) => {
res.json({ message: "Todo 已更新", data: req.body });
});
今天的重點是「讓 TypeScript 的型別進一步變成行為」。
我學會了:
以前要自己 if (!req.body.task)
一行行檢查,現在只要定義 DTO 規則,
整個驗證就變成模組化又乾淨。
而且這樣的結構在之後加上 Prisma、TypeORM 時也能直接套用。